import * as acorn from 'acorn'
import escodegen from 'escodegen'

import { readFile, writeFile } from 'fs/promises'

/**
 * 读取文件内容
 * @param {string} filePath 文件路径
 * @returns {Promise<string>} 文件内容
 */
const readFileContent = async filePath => {
  try {
    return await readFile(filePath, 'utf8')
  } catch (error) {
    console.error(`读取文件 ${filePath} 时出错:`, error)
    throw new Error(`无法读取文件 ${filePath}`)
  }
}

/**
 * 将空行替换为 console.log('line')
 * @param {string} data 文件内容
 * @returns {string} 替换后的文件内容
 */
const replaceEmptyLines = data => {
  const lines = data.split('\n')
  return lines.map(line => (line.trim() === '' ? "console.log('line');" : line)).join('\n')
}

/**
 * 解析代码为 AST 并保留注释和令牌
 * @param {string} data 代码内容
 * @returns {Object} 包含 AST、注释和令牌的对象
 */
const parseCode = data => {
  const comments = []
  const tokens = []
  const ast = acorn.parse(data, {
    ecmaVersion: 2020,
    locations: true,
    ranges: true,
    onComment: comments,
    onToken: tokens,
    sourceType: 'module',
  })
  escodegen.attachComments(ast, comments, tokens)
  return { ast, comments, tokens }
}

/**
 * 查找指定对象名的节点
 * @param {Object} ast 抽象语法树
 * @param {string} mapName 对象名
 * @returns {Object|null} 找到的对象节点，未找到则返回 null
 */
const findMapNameNode = (ast, mapName) => {
  for (const node of ast.body) {
    if (
      node.type === 'ExportNamedDeclaration' &&
      node.declaration &&
      node.declaration.type === 'VariableDeclaration' &&
      node.declaration.declarations[0].id.name === mapName
    ) {
      return node.declaration.declarations[0].init
    }
  }
  console.error(`未找到 ${mapName} 对象`)
  return null
}

/**
 * 往对象节点中添加键值对，若键已存在则不添加
 * @param {Object} mapNameNode 对象节点
 * @param {Object} newKeyValue 要添加的键值对
 * @returns {void}
 */
const addKeyValueToNode = (mapNameNode, newKeyValue) => {
  const existingKeys = mapNameNode.properties.map(property => property.key.name)
  for (const [key, value] of Object.entries(newKeyValue)) {
    if (existingKeys.includes(key)) {
      console.log(`键 ${key} 已存在，不添加。`)
      continue
    }
    let valueNode
    if (typeof value === 'function') {
      const funcCode = value.toString()
      const funcAst = parseCode(funcCode).ast.body[0].expression
      valueNode = funcAst
    } else if (typeof value === 'object') {
      valueNode = {
        type: 'ObjectExpression',
        properties: Object.entries(value).map(([subKey, subValue]) => ({
          type: 'Property',
          method: false,
          shorthand: false,
          computed: false,
          key: { type: 'Identifier', name: subKey },
          value: { type: 'Literal', value: subValue },
        })),
      }
    } else {
      valueNode = { type: 'Literal', value }
    }
    const property = {
      type: 'Property',
      method: false,
      shorthand: false,
      computed: false,
      key: { type: 'Identifier', name: key },
      value: valueNode,
    }
    mapNameNode.properties.push(property)
  }
}

/**
 * 将 console.log('line') 替换回空行
 * @param {string} newData 生成的代码内容
 * @returns {string} 替换后的代码内容
 */
const restoreEmptyLines = newData => {
  return newData.replace(/console\.log\('line'\);/g, '')
}

/**
 * 写入新文件
 * @param {string} outputFilePath 新文件路径
 * @param {string} newData 新文件内容
 * @returns {Promise<void>}
 */
const writeNewFile = async (outputFilePath, newData) => {
  try {
    await writeFile(outputFilePath, newData, 'utf8')
    console.log('文件已更新:', outputFilePath)
  } catch (error) {
    console.error(`更新文件 ${outputFilePath} 时出错:`, error)
    throw new Error(`无法更新文件 ${outputFilePath}`)
  }
}

/**
 * 往文件的某个对象插入键值对
 * @param {Object} param0
 * @param {string} param0.inputFilePath 原文件路径
 * @param {string} param0.outputFilePath 新文件路径
 * @param {Object} param0.newKeyValue 要添加的键值对
 * @param {string} param0.mapName 要添加的键值对所在的对象名
 * @returns {Promise<void>}
 */
export const addKeyValueToLoginRoutesMap = async ({ inputFilePath, outputFilePath, newKeyValue, mapName }) => {
  try {
    const data = await readFileContent(inputFilePath)
    const replacedData = replaceEmptyLines(data)
    const { ast } = parseCode(replacedData)
    const mapNameNode = findMapNameNode(ast, mapName)
    if (!mapNameNode) return
    addKeyValueToNode(mapNameNode, newKeyValue)
    const newData = escodegen.generate(ast, { comment: true })
    const finalData = restoreEmptyLines(newData)
    await writeNewFile(outputFilePath, finalData)
  } catch (err) {
    console.error('操作过程中出错:', err)
  }
}
